iT邦幫忙

2022 iThome 鐵人賽

DAY 3
1
Software Development

從 Node.js 開發者到量化交易者:打造屬於自己的投資系統系列 第 3

Day 03 - 由上而下投資法:先看大盤趨勢再選股

  • 分享至 

  • xImage
  •  

有德國股神之稱的 安德烈·科斯托蘭尼(André Kostolany)在其著作《一個投機者的告白》中提到:「在指數上漲過程中,即使是最差的投機人士也能賺到一些錢;而在指數下跌過程中,即使挑到好股票的人也賺不到錢。因此投資最看重的是普遍的趨勢,其次才是選股。

順應市場方向,多頭市場不做空,空頭市場不做多,是趨勢追隨者在市場生存的基本原則。

覆巢之下無完卵

大盤的風險,稱為 系統性風險市場風險,通常是受政治、經濟等大環境因素影響,導致所有股票受波及,因為這是無法靠分散投資消除的風險,所以又稱為 不可分散風險。我們觀察大盤走勢就是為了趨吉避凶,因為「覆巢之下無完卵」,當大盤表現不佳時,個股普遍都是下跌情形,所以科斯托蘭尼說:「根據經驗,沒有人的選股技術,可以好到即使股市普遍下跌也能賺到錢。

由上而下的投資策略

在本系列文中,我們將投資分為三個層次,分別是 大盤產業個股。如下圖所示,確認大盤處於上升趨勢後,再挑選表現強勁的產業,然後從產業中選擇理想的個股。

https://ithelp.ithome.com.tw/upload/images/20220903/201501509JdFOcY8fq.png

因此,在進行選股程序之前,我們會先著重於大盤分析,確認整體市場的方向是上升趨勢、下降趨勢或橫盤整理,然後再由上而下順勢而為。如同著名的股票作手 傑西.李佛摩 (Jessie Livermore) 提出的 最小阻力線 的概念:「在執行交易以前,確認市場的最小阻力線與你的操作同向後,才能建立操作部位。」

為了瞭解大盤趨勢,我們需要取得最近一段期間的大盤指數與市場成交資訊,以確認市場目前處在的位置。

「由上而下」或「由下而上」投資法並沒有好壞之分,不過投資與選股畢竟是主觀的過程,按筆者習慣以及文章編排方式,本系列文採用由上而下的投資策略。

查詢集中市場成交資訊

在證交所網站的 每日市場成交資訊 頁面,可以按年月份查詢集中市場成交資訊。

證交所首頁 > 交易資訊 > 盤後資訊 > 每日市場成交資訊

在「每日市場成交資訊」頁面,選擇「資料日期」按下「查詢」後,就會列出資料日期所屬年月份的資料。

https://ithelp.ithome.com.tw/upload/images/20220903/201501502tGxld9r0x.png

點擊「列印 / HTML」連結,瀏覽器會開新分頁將資訊輸出成可列印的 HTML 頁面。假設資料日期為「民國 111 年 07 月」,我們會得到以下 URL:

https://www.twse.com.tw/exchangeReport/FMTQIK?response=html&date=20220701

以上 URL 可設定的參數如下:

  • response:回應資料的格式。指定 html 輸出 HTML 文件;改為 csv 可以另存 CSV 檔案;設定成 json 或不指定則回應 JSON 格式資料。
  • date:資料日期。接受的日期格式為 yyyyMMdd,如 20220701。若不指定資料日期或輸入無效的日期格式,則預設回應最新年月份的市場成交資訊。

我們將 URL 查詢參數改為 response=json&date=20220701,證交所就會以 JSON 格式資料回應 2022 年 7 月 的集中市場成交資訊:

{
  "stat": "OK",
  "date": "20220701",
  "title": "111年07月市場成交資訊",
  "fields": [
    "日期",
    "成交股數",
    "成交金額",
    "成交筆數",
    "發行量加權股價指數",
    "漲跌點數"
  ],
  "data": [
    [
      "111/07/01",
      "7,861,837,121",
      "308,665,002,335",
      "2,761,795",
      "14,343.08",
      "-482.65"
    ]
  ],
  "notes": [
    "當日統計資訊含大盤、零股、盤後定價及鉅額交易,不含拍賣、標購。",
    "外幣成交值係以本公司當日下午3時30分公告匯率換算後加入成交金額。<br>公告匯率請參考本公司首頁>產品與服務>交易系統>雙幣ETF專區>代號對應及每日公告匯率。"
  ]
}

取得 JSON 格式資料後,我們要整理集中市場成交資訊就比較容易了,其他由證交所提供的盤後資訊大多都可以使用這種方式取得。

實作:取得集中市場成交資訊

在實作取得集中市場成交資訊的方法前,我們先安裝以下套件方便我們處理日期資料與數字格式的轉換:

$ npm install --save luxon numeral
$ npm install --save-dev @types/luxon @types/numeral

以上安裝的套件用途說明如下:

  • luxon:用於處理日期資料,由於知名套件 Moment.js 已不再維護,我們使用原團隊成員開發的 Luxon 作為替代品。
  • numeral:用於處理數字資料,我們從證交所及櫃買中心取得資料時,會遇到大量需要將 string 轉換成 number 型別的需求,使用該套件幫助我們做數字格式的轉換。
  • @types/luxon:提供 luxon 的型別定義檔 (Declaration Files)。
  • @types/numeral:提供 numeral 的型別定義檔 (Declaration Files)。

以上套件安裝完成後,開啟 src/scraper/twse-scraper.service.ts 檔案,在 TwseScraperService 實作 fetchMarketTrades() 方法,取得集中市場成交資訊:

import * as cheerio from 'cheerio';
import * as iconv from 'iconv-lite';
import * as numeral from 'numeral';
import { DateTime } from 'luxon';
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';

@Injectable()
export class TwseScraperService {
  constructor(private httpService: HttpService) {}

  ...

  async fetchMarketTrades(date: string) {
    // 將 `date` 轉換成 `yyyyMMdd` 格式
    const formattedDate = DateTime.fromISO(date).toFormat('yyyyMMdd');

    // 建立 URL 查詢參數
    const query = new URLSearchParams({
      response: 'json',     // 指定回應格式為 JSON
      date: formattedDate,  // 指定資料日期
    });
    const url = `https://www.twse.com.tw/exchangeReport/FMTQIK?${query}`;

    // 取得回應資料
    const responseData = await firstValueFrom(this.httpService.get(url))
      .then(response => (response.data.stat === 'OK') && response.data);

    // 若該日期非交易日或尚無成交資訊則回傳 null
    if (!responseData) return null;

    // 整理回應資料
    const data = responseData.data
      .map(row => {
        // [ 日期, 成交股數, 成交金額, 成交筆數, 發行量加權股價指數, 漲跌點數 ]
        const [ date, ...values ] = row;

        // 將 `民國年/MM/dd` 的日期格式轉換成 `yyyy-MM-dd`
        const [ year, month, day ] = date.split('/');
        const formatted = `${+year + 1911}${month}${day}`;
        const formattedDate = DateTime.fromFormat(formatted, 'yyyyMMdd').toISODate();

        // 轉為數字格式
        const [ tradeVolume, tradeValue, transaction, price, change ]
          = values.map(value => numeral(value).value());

        return {
          date: formattedDate,
          tradeVolume,
          tradeValue,
          transaction,
          price,
          change,
        };
      })
      .find(data => data.date === date) || null;  // 取得目標日期的成交資訊

    return data;
  }
}

fetchMarketTrades() 方法中,需要指定 date 參數,表示要取得市場成交資訊的日期。我們定義回傳的物件欄位包含如下:

  • date:日期
  • tradeVolume:成交股數
  • tradeValue:成交金額
  • transaction:成交筆數
  • price:發行量加權股價指數
  • change:漲跌點數

完成後,我們只要呼叫 TwseScraperServicefetchMarketTrades() 方法,就可以按日期取得集中市場成交資訊。以日期 2022-07-01 為例:

{
  date: '2022-07-01',
  tradeVolume: 7861837121,
  tradeValue: 308665002335,
  transaction: 2761795,
  price: 14343.08,
  change: -482.65
}

查詢櫃買市場成交資訊

在櫃買中心網站的 日成交量值指數 頁面,可以按年、月份查詢櫃買市場成交資訊。

櫃買中心首頁 > 上櫃 > 盤後資訊 > 日成交量值指數

在「日成交量值指數」頁面,選擇「資料年月」後,就會列出該年月份的資料。

https://ithelp.ithome.com.tw/upload/images/20220903/201501501xOfFm8qPW.png

點擊「列印/匯出HTML」連結,瀏覽器會開新分頁將資訊輸出成可列印的 HTML 頁面。假設資料年月為「111/07」,我們會得到以下 URL:

https://www.tpex.org.tw/web/stock/aftertrading/daily_trading_index/st41_result.php?l=zh-tw&d=111/07&s=0,asc,0&o=htm

以上 URL 可設定的參數如下:

  • l:輸出資料的語系。zh-tw 為正體中文;en-us 為英文。
  • d:資料年月,接受的日期格式為 民國年/MM,若指定成 民國年/MM/dd 也會得到跟 民國年/MM 一樣的結果。需要注意,若 l 參數指定為 en-us,則 d 參數需改為 yyyy/MM 的日期格式。
  • s:指定欄位依照升冪或降冪排序。例如 1,asc,0 是按成交股數(仟股)升冪排序;2,desc,0 則按金額(仟元)降冪排序,依此類推。
  • o:資料輸出的格式。指定 htm 表示輸出 HTML 文件;改為 csv 可以另存 CSV 檔案;設定成 json 或不指定則回應 JSON 格式資料。

我們將 URL 查詢參數改為 l=zh-tw&d=111/07&o=json,櫃買中心就會以 JSON 格式資料回應 2022 年 7 月的櫃買市場成交資訊:

{
  "reportDate": "111/07",
  "iTotalRecords": 1,
  "aaData": [
    [
      "111/07/01",
      "845,221,570",
      "67,600,392,538",
      "577,204",
      "173.03",
      "-8.06"
    ]
  ]
}

以上回應資料 aaData 欄位的陣列中每個索引值的元素,依序表示為日期、成交股數、成交金額、成交筆數、櫃買指數、漲跌點數。

取得 JSON 格式資料後,我們要整理櫃買市場成交資訊就比較容易了,其他由櫃買中心提供的盤後資訊大多都可以使用這種方式取得。

實作:取得櫃買市場成交資訊

我們新增一個 TpexScraperService 表示從櫃買中心取得資料的服務。打開終端機使用 Nest CLI 建立 TpexScraperService

$ nest g service scraper/tpex-scraper --flat --no-spec

Nest CLI 會在 src/scraper 目錄下建立 tpex-scraper.service.ts 檔案,並且將 TpexScraperService 加入至 ScraperModuleproviders 設定。

開啟 src/scraper/tpex-scraper.service.ts 檔案,在 TpexScraperService 實作 fetchMarketTrades() 方法,取得櫃買市場成交資訊:

import * as numeral from 'numeral';
import { DateTime } from 'luxon';
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';

@Injectable()
export class TpexScraperService {
  constructor(private httpService: HttpService) {}

async fetchMarketTrades(date: string) {
    // 將 `date` 轉換成 `民國年/MM` 格式
    const dt = DateTime.fromISO(date);
    const year = dt.get('year') - 1911;
    const formattedDate = `${year}/${dt.toFormat('MM')}`;

    // 建立 URL 查詢參數
    const query = new URLSearchParams({
      l: 'zh-tw',         // 指定語系為正體中文
      d: formattedDate,   // 指定日期
      o: 'json',          // 指定回應格式為 JSON
    });
    const url = `https://www.tpex.org.tw/web/stock/aftertrading/daily_trading_index/st41_result.php?${query}`;

    // 取得回應資料
    const responseData = await firstValueFrom(this.httpService.get(url))
      .then(response => (response.data.iTotalRecords > 0) && response.data);

    // 若該日期非交易日或尚無成交資訊則回傳 null
    if (!responseData) return null;

    // 整理回應資料
    const data = responseData.aaData.map(row => {
      // [ 日期, 成交股數, 金額, 筆數, 櫃買指數, 漲/跌 ]
      const [ date, ...values ] = row;

      // 將 `民國年/MM/dd` 的日期格式轉換成 `yyyy-MM-dd`
      const [ year, month, day ] = date.split('/');
      const formatted = `${+year + 1911}${month}${day}`;
      const formattedDate = DateTime.fromFormat(formatted, 'yyyyMMdd').toISODate();

      // 轉為數字格式
      const [ tradeVolume, tradeValue, transaction, price, change ]
        = values.map(value => numeral(value).value());

      return {
        date: formattedDate,
        tradeVolume,
        tradeValue,
        transaction,
        price,
        change,
      };
    })
    .find(data => data.date === date) || null;  // 取得目標日期的成交資訊

    return data;
  }
}

fetchMarketTrades() 方法中,需要指定 date 參數,表示要取得市場成交資訊的日期。我們定義回傳的物件欄位包含如下:

  • date:日期
  • tradeVolume:成交股數
  • tradeValue:成交金額
  • transaction:成交筆數
  • price:櫃買指數
  • change:漲跌點數

完成後,我們只要呼叫 TpexScraperServicefetchMarketTrades() 方法,就可以按日期取得櫃買市場成交資訊。以日期 2022-07-01 為例:

{
  date: '2022-07-01',
  tradeVolume: 845221570,
  tradeValue: 67600392538,
  transaction: 577204,
  price: 173.03,
  change: -8.06
}

小心!大盤指數可能失真

大盤指數是衡量整體股票市場漲跌的重要指標,集中市場的「加權指數」與櫃買市場的「櫃買指數」都是以股票的市值加權計算,市值較高的股票加權比較高,因此權值股的漲跌對於大盤指數有舉足輕重的影響。

https://ithelp.ithome.com.tw/upload/images/20220903/20150150lkbXgYkf1j.png

Source:富果熱力圖

上圖是 2022 年上半年加權指數成分股按市值大小呈現的熱力圖,我們可以一眼看出「護國神山」台積電(2330)佔了非常大的面積板塊!根據 2022 年 8 月期交所 臺灣證券交易所發行量加權股價指數成分股暨市值比重 的資料顯示,臺股市值第一名的台積電佔大盤比重高達 28%,第二名的鴻海(2317)則是 3.23%,一、二名市值相差之大,台積電稱為「護國神山」當之無愧。因爲台積電市值佔大盤比重非常高,所以台積電在盤中的大漲或大跌,足以帶動整個大盤走勢。

當加權指數收紅,是因為台積電獨強,但其他中小型個股普遍表現不佳時,這個盤勢常被股民戲稱為「拉積盤」。當這個情況發生時,加權指數可能不足以代表整體市場,因此有些資訊廠商甚至設計出「大盤扣除台積電」的指數計算方式。

上漲家數及下跌家數

除了大盤指數外,股市的 上漲家數下跌家數 也是另一種衡量市場表現的方式,這通常被稱為 市場寬幅(Market Breadth)指摽。以 2022 年初至 8 月 31 日的集中市場為例,共發生 12 次加權指數上漲,但上漲家數卻小於下跌家數的情形!尤其在 2022 年 1 月 4 日、2 月 10 日及 7 月 15 日,加權指數上漲超過百點,但整體市場的下跌家數卻比上漲家數還要多,此時投資人的持股部位若沒有權值股,對於大盤指數上漲可能會「無感」。

日期 加權指數 漲跌 漲跌幅 上漲家數 下跌家數
2022-01-04 18526.35 +255.84 +1.40% 380 460
2022-01-11 18288.21 +48.83 +0.27% 236 637
2022-01-12 18375.40 +87.19 +0.48% 420 435
2022-01-13 18436.93 +61.53 +0.33% 412 433
2022-01-24 17989.04 +89.74 +0.50% 278 585
2022-02-10 18338.05 +186.29 +1.03% 385 449
2022-03-29 17548.66 +28.65 +0.16% 422 425
2022-07-15 14550.62 +112.10 +0.78% 401 469
2022-07-20 14733.22 +39.14 +0.27% 406 454
2022-07-22 14949.36 +11.66 +0.08% 418 440
2022-08-03 14777.02 +29.79 +0.20% 188 700
2022-08-16 15420.57 +3.22 +0.02% 395 445

Source:臺灣證券交易所

當大盤指數可能無法反映整體市況時,這時候觀察股票市場的上漲及下跌家數就會比較客觀。許多用來衡量市場寬幅的技術指標如 騰落指標(ADL)、漲跌比率(ADR)、超買超賣指標(OBOS)等,都是利用市場每個交易日的上漲家數及下跌家數計算而來,藉此反映整體市場行情漲升力道與強弱變化,進一步研判出未來行情可能的發展方向。

查詢集中市場上漲及下跌家數

在證交所網站的 每日收盤行情 頁面,可以按日期查詢集中市場上漲及下跌家數。

證交所首頁 > 交易資訊 > 盤後資訊 > 每日收盤行情

在「每日收盤行情」頁面,選取「日期」並在「分類項目」選擇「大盤統計資訊」後按下「查詢」,就會列出該日大盤統計資訊。

https://ithelp.ithome.com.tw/upload/images/20220903/20150150n6SslHyJy2.png

在頁面下方可以找到「漲跌證券數合計」。

https://ithelp.ithome.com.tw/upload/images/20220903/20150150Hksa5Y6hzx.png

點擊「列印 / HTML」連結,瀏覽器會開新分頁將資訊輸出成可列印的 HTML 頁面。假設資料日期為「民國 111 年 07 月 01 日」,我們會得到以下 URL:

https://www.twse.com.tw/exchangeReport/MI_INDEX?response=html&date=20220701&type=MS

以上 URL 可設定的參數如下:

  • response:回應資料的格式。指定 html 輸出 HTML 文件;改為 csv 可以另存 CSV 檔案;設定成 json 或不指定則回應 JSON 格式資料。
  • date:資料日期。接受的日期格式為 yyyyMMdd,如 20220701
  • type:分類項目。MS 代表「大盤統計資訊」。

我們將 URL 查詢參數改為 response=json&date=20220701,證交所就會以 JSON 格式資料回應 2022 年 7 月 1 日的大盤統計資訊:

{
  "fields8": [
    "類型",
    "整體市場",
    "股票"
  ],
  "subtitle8": "漲跌證券數合計",
  "data8": [
    [
      "上漲(漲停)",
      "2,375(4)",
      "58(1)"
    ],
    [
      "下跌(跌停)",
      "6,635(157)",
      "873(15)"
    ],
    [
      "持平",
      "199",
      "22"
    ],
    [
      "未成交",
      "18,478",
      "1"
    ],
    [
      "無比價",
      "1,968",
      "11"
    ]
  ],
  ......
}

因為回應資料較長,以上我們簡化了資料只顯示重要的部分。JSON 欄位 data8 的內容即是漲跌證券數合計。「整體市場」包含了權證,權證又包含了看多的認購權證(Call Warrant)以及看空的認售權證(Put Warrant),無法實際判斷大盤的多空情形,因此我們只需要取得「股票」欄的上漲及下跌家數即可。

若在 URL 的 date 參數輸入了非交易日日期或資料尚未更新,則證交所會回應:

{
  "stat": "很抱歉,沒有符合條件的資料!"
}

實作:取得集中市場上漲及下跌家數

開啟 src/scraper/twse-scraper.service.ts 檔案,在 TwseScraperService 實作 fetchMarketBreadth() 方法,取得集中市場上漲及下跌家數:

import * as cheerio from 'cheerio';
import * as iconv from 'iconv-lite';
import * as numeral from 'numeral';
import { DateTime } from 'luxon';
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';

@Injectable()
export class TwseScraperService {
  constructor(private httpService: HttpService) {}

  ...

  async fetchMarketBreadth(date: string) {
    // 將 `date` 轉換成 `yyyyMMdd` 格式
    const formattedDate = DateTime.fromISO(date).toFormat('yyyyMMdd');

    // 建立 URL 查詢參數
    const query = new URLSearchParams({
      response: 'json',     // 指定回應格式為 JSON
      date: formattedDate,  // 指定資料日期
      type: 'MS',           // 指定類別為大盤統計資訊
    });
    const url = `https://www.twse.com.tw/exchangeReport/MI_INDEX?${query}`;

    // 取得回應資料
    const responseData = await firstValueFrom(this.httpService.get(url))
      .then(response => (response.data.stat === 'OK') && response.data);

    // 若該日期非交易日或尚無成交資訊則回傳 null
    if (!responseData) return null;

    // 整理回應資料
    const raw = responseData.data8.map(row => row[2]);  // 取股票市場統計
    const [ up, limitUp, down, limitDown, unchanged, unmatched, notApplicable ] = [
      ...raw[0].replace(')', '').split('('),  // 取出漲停家數
      ...raw[1].replace(')', '').split('('),  // 取出漲停家數
      ...raw.slice(2),
    ].map(value => numeral(value).value());   // 轉為數字格式

    const data = {
      date,
      up,
      limitUp,
      down,
      limitDown,
      unchanged,
      unmatched: unmatched + notApplicable, // 未成交(含暫停交易)家數
    };

    return data;
  }
}

fetchMarketBreadth() 方法中,需要指定 date 參數,表示要取得上漲及下跌家數的日期。我們定義回傳的物件欄位包含如下:

  • date:日期
  • up:上漲家數
  • limitUp:漲停家數
  • down:下跌家數
  • limitDown:跌停家數
  • unchanged:平盤家數
  • unmatched:未成交家數

完成後,我們只要呼叫 TwseScraperServicefetchMarketBreadth() 方法,就可以按日期取得集中市場上漲及下跌家數。以日期 2022-07-01 為例:

{
  date: '2022-07-01',
  up: 58,
  limitUp: 1,
  down: 873,
  limitDown: 15,
  unchanged: 22,
  unmatched: 12
}

查詢櫃買市場上漲及下跌家數

在櫃買中心網站的 上櫃股票市場現況 頁面,可以按日期查詢櫃買市場上漲及下跌家數。

櫃買中心首頁 > 上櫃 > 盤後資訊 > 上櫃股票市場現況

在「上櫃股票市場現況」頁面,選取「資料日期」後,就會列出該日市場現況,並找到上漲及下跌家數統計。

https://ithelp.ithome.com.tw/upload/images/20220903/201501504q3YRBmyhK.png

點擊「列印/匯出HTML」連結,瀏覽器會開新分頁將資訊輸出成可列印的 HTML 頁面。假設資料年月為「111/07/01」,我們會得到以下 URL:

https://www.tpex.org.tw/web/stock/aftertrading/market_highlight/highlight_result.php?l=zh-tw&o=htm&d=111/07/01

以上 URL 可設定的參數如下:

  • l:輸出資料的語系。zh-tw 為正體中文;en-us 為英文。
  • o:資料輸出的格式。指定 htm 表示輸出 HTML 文件;改為 csv 可以另存 CSV 檔案;設定成 json 或不指定則回應 JSON 格式資料。
  • d:資料日期,接受 民國年/月/日 的日期格式。需要注意,若 l 參數指定為 en-us,則 d 參數需改成 西元年/月/日 的日期格式。

我們將 URL 查詢參數改為 l=zh-tw&d=111/07/01&o=json,櫃買中心就會以 JSON 格式資料回應 111 年 7 月 1 日的上櫃市場現況:

{
  "reportDate": "111/07/01",
  "iTotalRecords": 9,
  "listedNum": "799",
  "capital": "769,889",
  "companyValue": "4,240,980",
  "tradeAmount": "67,619",
  "tradeVolumn": "845,321",
  "close": "173.03",
  "change": "-8.06",
  "upNum": "76",
  "upStopNum": "0",
  "downNum": "689",
  "downStopNum": "27",
  "noChangeNum": "22",
  "noTradeNum": "12",
  "rptNote": "標的物:等價與鉅額成交之股票、換股權利證書,不含可轉債、認購售權證、認股權憑證及不動產受益憑證"
}

以上資料欄位 upNum 為上漲家數;upStopNum 為漲停家數;downNum 為下跌家數;downStopNum 為跌停家數;noChangeNum 為平盤家數;noTradeNum 為未成交(含暫停交易)家數。

實作:取得櫃買市場上漲及下跌家數

開啟 src/scraper/tpex-scraper.service.ts 檔案,在 TpexScraperService 實作 fetchMarketBreadth() 方法,取得櫃買市場上漲及下跌家數:

import * as numeral from 'numeral';
import { DateTime } from 'luxon';
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';

@Injectable()
export class TpexScraperService {
  constructor(private httpService: HttpService) {}

  ...

  async fetchMarketBreadth(date: string) {
    // `date` 轉換成 `民國年/MM/dd` 格式
    const dt = DateTime.fromISO(date);
    const year = dt.get('year') - 1911;
    const formattedDate = `${year}/${dt.toFormat('MM/dd')}`;

    // 建立 URL 查詢參數
    const query = new URLSearchParams({
      l: 'zh-tw',         // 指定語系為正體中文
      d: formattedDate,   // 指定日期
      o: 'json',          // 指定回應格式為 JSON
    });
    const url = `https://www.tpex.org.tw/web/stock/aftertrading/market_highlight/highlight_result.php?${query}`;

    // 取得回應資料
    const responseData = await firstValueFrom(this.httpService.get(url))
      .then(response => (response.data.iTotalRecords > 0) && response.data);

    // 若該日期非交易日或尚無成交資訊則回傳 null
    if (!responseData) return null;

    // 整理回應資料
    const { upNum, upStopNum, downNum, downStopNum, noChangeNum, noTradeNum } = responseData;
    const [ up, limitUp, down, limitDown, unchanged, unmatched ] = [
      upNum, upStopNum, downNum, downStopNum, noChangeNum, noTradeNum
    ].map(value => numeral(value).value());   // 轉為數字格式

    const data = {
      date,
      up,
      limitUp,
      down,
      limitDown,
      unchanged,
      unmatched,
    };

    return data;
  }
}

fetchMarketBreadth() 方法中,需要指定 date 參數,表示要取得上漲及下跌家數的日期。我們定義回傳的物件欄位包含如下:

  • date:日期
  • up:上漲家數
  • limitUp:漲停家數
  • down:下跌家數
  • limitDown:跌停家數
  • unchanged:平盤家數
  • unmatched:未成交家數

完成後,我們只要呼叫 TpexScraperServicefetchMarketBreadth() 方法,就可以按日期取得櫃買市場上漲及下跌家數。以日期 2022-07-01 為例:

{
  date: '2022-07-01',
  up: 76,
  limitUp: 0,
  down: 689,
  limitDown: 27,
  unchanged: 22,
  unmatched: 12  
}

本日小結

  • 由上而下的投資策略是確認大盤處於上升趨勢後,再挑選表現強勁的產業,然後從產業中選擇理想的個股。
  • 為避免大盤指數可能受大型權值股影響失真,可一併觀察市場的上漲家數及下跌家數。
  • 瞭解如何在證交所網站上查詢並實作取得集中市場成交資訊的方法。
  • 瞭解如何在櫃買中心網站上查詢並實作取得櫃買市場成交資訊的方法。
  • 瞭解如何在證交所網站上查詢並實作取得集中市場上漲家數及下跌家數的方法。
  • 瞭解如何在櫃買中心網站上查詢並實作取得櫃買市場上漲家數及下跌家數的方法。

Node.js 量化投資全攻略:從資料收集到自動化交易系統建構實戰
本系列文已正式出版為《Node.js 量化投資全攻略:從資料收集到自動化交易系統建構實戰》。本書新增了全新內容和實用範例,為你提供更深入的學習體驗!歡迎參考選購,開始你的量化投資之旅!
天瓏網路書店連結:https://www.tenlong.com.tw/products/9786263336070


上一篇
Day 02 - 數據即財富:股市資料來源與取得
下一篇
Day 04 - 法人主導的市場:三大法人買賣超
系列文
從 Node.js 開發者到量化交易者:打造屬於自己的投資系統31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Marvin
iT邦新手 2 級 ‧ 2022-09-03 09:01:51

這篇文章好棒!/images/emoticon/emoticon33.gif

我要留言

立即登入留言